<template>
<div :class="wrapperClasses" v-click-outside:mousedown.capture="handleClose" v-click-outside.capture="handleClose">
    <div ref="reference" :class="[prefixCls + '-rel']">
        <slot>
            <i-input :key="forceInputRerender" :element-id="elementId" :class="[prefixCls + '-editor']" :readonly="!editable || readonly" :disabled="disabled" :size="size" :placeholder="placeholder" :value="visualValue" :name="name" ref="input" @on-input-change="handleInputChange"
                @on-focus="handleFocus" @on-blur="handleBlur" @on-click="handleIconClick" @click.native="handleFocus" @keydown.native="handleKeydown" @mouseenter.native="handleInputMouseenter" @mouseleave.native="handleInputMouseleave" :icon="iconType"></i-input>
        </slot>
    </div>
    <transition name="transition-drop">
        <Drop @click.native="handleTransferClick" v-show="opened" :class="{ [prefixCls + '-transfer']: transfer }" :placement="placement" ref="drop" :data-transfer="transfer" v-transfer-dom>
            <div>
                <component :is="panel" ref="pickerPanel" :visible="visible" :showTime="type === 'datetime' || type === 'datetimerange'" :confirm="isConfirm" :selectionMode="selectionMode" :steps="steps" :format="format" :value="internalValue" :start-date="startDate"
                    :split-panels="splitPanels" :show-week-numbers="showWeekNumbers" :picker-type="type" :multiple="multiple" :focused-date="focusedDate" :time-picker-options="timePickerOptions" v-bind="ownPickerProps" @on-pick="onPick" @on-pick-clear="handleClear"
                    @on-pick-success="onPickSuccess" @on-pick-click="disableClickOutSide = true" @on-selection-mode-change="onSelectionModeChange"></component>
            </div>
        </Drop>
    </transition>
</div>
</template>

<script>
import Drop from './select/dropdown.vue';
import {
    directive as clickOutside
} from 'v-click-outside-x';
import TransferDom from './directives/transfer-dom';
import {
    oneOf
} from './utils/assist';
import {
    DEFAULT_FORMATS,
    RANGE_SEPARATOR,
    TYPE_VALUE_RESOLVER_MAP,
    getDayCountOfMonth
} from './util';
import {
    findComponentsDownward
} from './utils/assist';
import Emitter from './mixins/emitter';

const prefixCls = 'ivu-date-picker';
const pickerPrefixCls = 'ivu-picker';

const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
const keyValueMapper = {
    40: 'up',
    39: 'right',
    38: 'down',
    37: 'left',
};

const mapPossibleValues = (key, horizontal, vertical) => {
    if (key === 'left') return horizontal * -1;
    if (key === 'right') return horizontal * 1;
    if (key === 'up') return vertical * 1;
    if (key === 'down') return vertical * -1;
};

const pulseElement = (el) => {
    const pulseClass = 'ivu-date-picker-btn-pulse';
    el.classList.add(pulseClass);
    setTimeout(() => el.classList.remove(pulseClass), 200);
};

const extractTime = date => {
    if (!date) return [0, 0, 0];
    return [
        date.getHours(), date.getMinutes(), date.getSeconds()
    ];
};


export default {
    mixins: [Emitter],
    components: {
        Drop
    },
    directives: {
        clickOutside,
        TransferDom
    },
    props: {
        format: {
            type: String
        },
        readonly: {
            type: Boolean,
            default: false
        },
        disabled: {
            type: Boolean,
            default: false
        },
        editable: {
            type: Boolean,
            default: true
        },
        clearable: {
            type: Boolean,
            default: true
        },
        confirm: {
            type: Boolean,
            default: false
        },
        open: {
            type: Boolean,
            default: null
        },
        multiple: {
            type: Boolean,
            default: false
        },
        timePickerOptions: {
            default: () => ({}),
            type: Object,
        },
        splitPanels: {
            type: Boolean,
            default: false
        },
        showWeekNumbers: {
            type: Boolean,
            default: false
        },
        startDate: {
            type: Date
        },
        size: {
            validator(value) {
                return oneOf(value, ['small', 'large', 'default']);
            }
        },
        placeholder: {
            type: String,
            default: ''
        },
        placement: {
            validator(value) {
                return oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']);
            },
            default: 'bottom-start'
        },
        transfer: {
            type: Boolean,
            default: false
        },
        name: {
            type: String
        },
        elementId: {
            type: String
        },
        steps: {
            type: Array,
            default: () => []
        },
        value: {
            type: [Date, String, Array]
        },
        options: {
            type: Object,
            default: () => ({})
        }
    },
    data() {
        const isRange = this.type.includes('range');
        const emptyArray = isRange ? [null, null] : [null];
        const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
        const focusedTime = initialValue.map(extractTime);

        return {
            prefixCls: prefixCls,
            showClose: false,
            visible: false,
            internalValue: initialValue,
            disableClickOutSide: false, // fixed when click a date,trigger clickoutside to close picker
            disableCloseUnderTransfer: false, // transfer 模式下，点击Drop也会触发关闭,
            selectionMode: this.onSelectionModeChange(this.type),
            forceInputRerender: 1,
            isFocused: false,
            focusedDate: initialValue[0] || this.startDate || new Date(),
            focusedTime: {
                column: 0, // which column inside the picker
                picker: 0, // which picker
                time: focusedTime, // the values array into [hh, mm, ss],
                active: false
            },
            internalFocus: false,
        };
    },
    computed: {
        wrapperClasses() {
            return [prefixCls, {
                [prefixCls + '-focused']: this.isFocused
            }];
        },
        publicVModelValue() {
            if (this.multiple) {
                return this.internalValue.slice();
            } else {
                const isRange = this.type.includes('range');
                let val = this.internalValue.map(date => date instanceof Date ? new Date(date) : (date || ''));

                if (this.type.match(/^time/)) val = val.map(this.formatDate);
                return (isRange || this.multiple) ? val : val[0];
            }
        },
        publicStringValue() {
            const {
                formatDate,
                publicVModelValue,
                type
            } = this;
            if (type.match(/^time/)) return publicVModelValue;
            if (this.multiple) return formatDate(publicVModelValue);
            return Array.isArray(publicVModelValue) ? publicVModelValue.map(formatDate) : formatDate(publicVModelValue);
        },
        opened() {
            return this.open === null ? this.visible : this.open;
        },
        iconType() {
            let icon = 'ios-calendar-outline';
            if (this.type === 'time' || this.type === 'timerange') icon = 'ios-clock-outline';
            if (this.showClose) icon = 'ios-close';
            return icon;
        },
        transition() {
            const bottomPlaced = this.placement.match(/^bottom/);
            return bottomPlaced ? 'slide-up' : 'slide-down';
        },
        visualValue() {
            return this.formatDate(this.internalValue);
        },
        isConfirm() {
            return this.confirm || this.type === 'datetime' || this.type === 'datetimerange' || this.multiple;
        }
    },
    methods: {
        onSelectionModeChange(type) {
            if (type.match(/^date/)) type = 'date';
            this.selectionMode = oneOf(type, ['year', 'month', 'date', 'time']) && type;
            return this.selectionMode;
        },
        // 开启 transfer 时，点击 Drop 即会关闭，这里不让其关闭
        handleTransferClick() {
            if (this.transfer) this.disableCloseUnderTransfer = true;
        },
        handleClose(e) {
            if (this.disableCloseUnderTransfer) {
                this.disableCloseUnderTransfer = false;
                return false;
            }

            if (e && e.type === 'mousedown' && this.visible) {
                e.preventDefault();
                e.stopPropagation();
                return;
            }

            if (this.visible) {
                const pickerPanel = this.$refs.pickerPanel && this.$refs.pickerPanel.$el;
                if (e && pickerPanel && pickerPanel.contains(e.target)) return; // its a click inside own component, lets ignore it.

                this.visible = false;
                e && e.preventDefault();
                e && e.stopPropagation();
                return;
            }

            this.isFocused = false;
            this.disableClickOutSide = false;
        },
        handleFocus(e) {
            if (this.readonly) return;
            this.isFocused = true;
            if (e && e.type === 'focus') return; // just focus, don't open yet
            this.visible = true;
        },
        handleBlur(e) {
            if (this.internalFocus) {
                this.internalFocus = false;
                return;
            }
            if (this.visible) {
                e.preventDefault();
                return;
            }

            this.isFocused = false;
            this.onSelectionModeChange(this.type);
            this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
            this.reset();
            this.$refs.pickerPanel.onToggleVisibility(false);

        },
        handleKeydown(e) {
            const keyCode = e.keyCode;

            // handle "tab" key
            if (keyCode === 9) {
                if (this.visible) {
                    e.stopPropagation();
                    e.preventDefault();

                    if (this.isConfirm) {
                        const selector = `.${pickerPrefixCls}-confirm > *`;
                        const tabbable = this.$refs.drop.$el.querySelectorAll(selector);
                        this.internalFocus = true;
                        const element = [...tabbable][e.shiftKey ? 'pop' : 'shift']();
                        element.focus();
                    } else {
                        this.handleClose();
                    }
                } else {
                    this.focused = false;
                }
            }

            // open the panel
            const arrows = [37, 38, 39, 40];
            if (!this.visible && arrows.includes(keyCode)) {
                this.visible = true;
                return;
            }

            // close on "esc" key
            if (keyCode === 27) {
                if (this.visible) {
                    e.stopPropagation();
                    this.handleClose();
                }
            }

            // select date, "Enter" key
            if (keyCode === 13) {
                const timePickers = findComponentsDownward(this, 'TimeSpinner');
                if (timePickers.length > 0) {
                    const columnsPerPicker = timePickers[0].showSeconds ? 3 : 2;
                    const pickerIndex = Math.floor(this.focusedTime.column / columnsPerPicker);
                    const value = this.focusedTime.time[pickerIndex];

                    timePickers[pickerIndex].chooseValue(value);
                    return;
                }

                if (this.type.match(/range/)) {
                    this.$refs.pickerPanel.handleRangePick(this.focusedDate, 'date');
                } else {
                    const panels = findComponentsDownward(this, 'PanelTable');
                    const compareDate = (d) => {
                        const sliceIndex = ['year', 'month', 'date'].indexOf((this.type)) + 1;
                        return [d.getFullYear(), d.getMonth(), d.getDate()].slice(0, sliceIndex).join('-');
                    };
                    const dateIsValid = panels.find(({
                        cells
                    }) => {
                        return cells.find(({
                            date,
                            disabled
                        }) => compareDate(date) === compareDate(this.focusedDate) && !disabled);
                    });
                    if (dateIsValid) this.onPick(this.focusedDate, false, 'date');
                }
            }

            if (!arrows.includes(keyCode)) return; // ignore rest of keys

            // navigate times and dates
            if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving
            this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey);
        },
        reset() {
            this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
        },
        navigateTimePanel(direction) {

            this.focusedTime.active = true;
            const horizontal = direction.match(/left|right/);
            const vertical = direction.match(/up|down/);
            const timePickers = findComponentsDownward(this, 'TimeSpinner');

            const maxNrOfColumns = (timePickers[0].showSeconds ? 3 : 2) * timePickers.length;
            const column = (currentColumn => {
                const incremented = currentColumn + (horizontal ? (direction === 'left' ? -1 : 1) : 0);
                return (incremented + maxNrOfColumns) % maxNrOfColumns;
            })(this.focusedTime.column);

            const columnsPerPicker = maxNrOfColumns / timePickers.length;
            const pickerIndex = Math.floor(column / columnsPerPicker);
            const col = column % columnsPerPicker;


            if (horizontal) {
                const time = this.internalValue.map(extractTime);

                this.focusedTime = {
                    ...this.focusedTime,
                    column: column,
                    time: time
                };
                timePickers.forEach((instance, i) => {
                    if (i === pickerIndex) instance.updateFocusedTime(col, time[pickerIndex]);
                    else instance.updateFocusedTime(-1, instance.focusedTime);
                });
            }

            if (vertical) {
                const increment = direction === 'up' ? 1 : -1;
                const timeParts = ['hours', 'minutes', 'seconds'];


                const pickerPossibleValues = timePickers[pickerIndex][`${timeParts[col]}List`];
                const nextIndex = pickerPossibleValues.findIndex(({
                    text
                }) => this.focusedTime.time[pickerIndex][col] === text) + increment;
                const nextValue = pickerPossibleValues[nextIndex % pickerPossibleValues.length].text;
                const times = this.focusedTime.time.map((time, i) => {
                    if (i !== pickerIndex) return time;
                    time[col] = nextValue;
                    return time;
                });
                this.focusedTime = {
                    ...this.focusedTime,
                    time: times
                };

                timePickers.forEach((instance, i) => {
                    if (i === pickerIndex) instance.updateFocusedTime(col, times[i]);
                    else instance.updateFocusedTime(-1, instance.focusedTime);
                });
            }
        },
        navigateDatePanel(direction, shift) {

            const timePickers = findComponentsDownward(this, 'TimeSpinner');
            if (timePickers.length > 0) {
                // we are in TimePicker mode
                this.navigateTimePanel(direction, shift, timePickers);
                return;
            }

            if (shift) {
                if (this.type === 'year') {
                    this.focusedDate = new Date(
                        this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 10),
                        this.focusedDate.getMonth(),
                        this.focusedDate.getDate()
                    );
                } else {
                    this.focusedDate = new Date(
                        this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 1),
                        this.focusedDate.getMonth() + mapPossibleValues(direction, 1, 0),
                        this.focusedDate.getDate()
                    );
                }

                const position = direction.match(/left|down/) ? 'prev' : 'next';
                const double = direction.match(/up|down/) ? '-double' : '';

                // pulse button
                const button = this.$refs.drop.$el.querySelector(`.ivu-date-picker-${position}-btn-arrow${double}`);
                if (button) pulseElement(button);
                return;
            }

            const initialDate = this.focusedDate || (this.internalValue && this.internalValue[0]) || new Date();
            const focusedDate = new Date(initialDate);

            if (this.type.match(/^date/)) {
                const lastOfMonth = getDayCountOfMonth(initialDate.getFullYear(), initialDate.getMonth());
                const startDay = initialDate.getDate();
                const nextDay = focusedDate.getDate() + mapPossibleValues(direction, 1, 7);

                if (nextDay < 1) {
                    if (direction.match(/left|right/)) {
                        focusedDate.setMonth(focusedDate.getMonth() + 1);
                        focusedDate.setDate(nextDay);
                    } else {
                        focusedDate.setDate(startDay + Math.floor((lastOfMonth - startDay) / 7) * 7);
                    }
                } else if (nextDay > lastOfMonth) {
                    if (direction.match(/left|right/)) {
                        focusedDate.setMonth(focusedDate.getMonth() - 1);
                        focusedDate.setDate(nextDay);
                    } else {
                        focusedDate.setDate(startDay % 7);
                    }
                } else {
                    focusedDate.setDate(nextDay);
                }
            }

            if (this.type.match(/^month/)) {
                focusedDate.setMonth(focusedDate.getMonth() + mapPossibleValues(direction, 1, 3));
            }

            if (this.type.match(/^year/)) {
                focusedDate.setFullYear(focusedDate.getFullYear() + mapPossibleValues(direction, 1, 3));
            }

            this.focusedDate = focusedDate;
        },
        handleInputChange(event) {
            const isArrayValue = this.type.includes('range') || this.multiple;
            const oldValue = this.visualValue;
            const newValue = event.target.value;
            const newDate = this.parseDate(newValue);
            const disabledDateFn =
                this.options &&
                typeof this.options.disabledDate === 'function' &&
                this.options.disabledDate;
            const valueToTest = isArrayValue ? newDate : newDate[0];
            const isDisabled = disabledDateFn && disabledDateFn(valueToTest);
            const isValidDate = newDate.reduce((valid, date) => valid && date instanceof Date, true);

            if (newValue !== oldValue && !isDisabled && isValidDate) {
                this.emitChange(this.type);
                this.internalValue = newDate;
            } else {
                this.forceInputRerender++;
            }
        },
        handleInputMouseenter() {
            if (this.readonly || this.disabled) return;
            if (this.visualValue && this.clearable) {
                this.showClose = true;
            }
        },
        handleInputMouseleave() {
            this.showClose = false;
        },
        handleIconClick() {
            if (this.showClose) {
                this.handleClear();
            } else if (!this.disabled) {
                this.handleFocus();
            }
        },
        handleClear() {
            this.visible = false;
            this.internalValue = this.internalValue.map(() => null);
            this.$emit('on-clear');
            this.dispatch('FormItem', 'on-form-change', '');
            this.emitChange(this.type);
            this.reset();

            setTimeout(
                () => this.onSelectionModeChange(this.type),
                500 // delay to improve dropdown close visual effect
            );
        },
        emitChange(type) {
            this.$nextTick(() => {
                this.$emit('on-change', this.publicStringValue, type);
                this.dispatch('FormItem', 'on-form-change', this.publicStringValue);
            });
        },
        parseDate(val) {
            const isRange = this.type.includes('range');
            const type = this.type;
            const parser = (
                TYPE_VALUE_RESOLVER_MAP[type] ||
                TYPE_VALUE_RESOLVER_MAP['default']
            ).parser;
            const format = this.format || DEFAULT_FORMATS[type];
            const multipleParser = TYPE_VALUE_RESOLVER_MAP['multiple'].parser;

            if (val && type === 'time' && !(val instanceof Date)) {
                val = parser(val, format);
            } else if (this.multiple && val) {
                val = multipleParser(val, format);
            } else if (isRange) {
                if (!val) {
                    val = [null, null];
                } else {
                    if (typeof val === 'string') {
                        val = parser(val, format);
                    } else if (type === 'timerange') {
                        val = parser(val, format).map(v => v || '');
                    } else {
                        const [start, end] = val;
                        if (start instanceof Date && end instanceof Date) {
                            val = val.map(date => new Date(date));
                        } else if (typeof start === 'string' && typeof end === 'string') {
                            val = parser(val.join(RANGE_SEPARATOR), format);
                        } else if (!start || !end) {
                            val = [null, null];
                        }
                    }
                }
            } else if (typeof val === 'string' && type.indexOf('time') !== 0) {
                val = parser(val, format) || null;
            }

            return (isRange || this.multiple) ? (val || []) : [val];
        },
        formatDate(value) {
            const format = DEFAULT_FORMATS[this.type];

            if (this.multiple) {
                const formatter = TYPE_VALUE_RESOLVER_MAP.multiple.formatter;
                return formatter(value, this.format || format);
            } else {
                const {
                    formatter
                } = (
                    TYPE_VALUE_RESOLVER_MAP[this.type] ||
                    TYPE_VALUE_RESOLVER_MAP['default']
                );
                return formatter(value, this.format || format);
            }
        },
        onPick(dates, visible = false, type) {

            // 只做周选择器控制，懒得做通用的
            this.internalValue = []
            // 获取当年第一天
            let myDate = new Date(dates.year, 0, 1)
            // 获取第一天是星期几
            let week = myDate.getDay()
            // 设置后续循环同一星期往后的天数
            for (let i = 0; i < 8 - week; i++) {
                let myDate = new Date(dates.year, 0, 1)
                myDate.setDate(myDate.getDate() + (dates.week - 1) * 7 + i)
                this.internalValue.push(myDate)
            }
            // 设置前置循环同一星期往前的天数
            for (let i = 1; i < week; i++) {
                let myDate = new Date(dates.year, 0, 1)
                myDate.setDate(myDate.getDate() + (dates.week - 1) * 7 - i)
                this.internalValue.unshift(myDate)
            }

            if (this.internalValue[0]) this.focusedDate = this.internalValue[0];
            this.focusedTime = {
                ...this.focusedTime,
                time: this.internalValue.map(extractTime)
            };

            if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode
            if (!this.isConfirm) this.visible = visible;
            this.emitChange(type);
        },
        onPickSuccess() {
            this.visible = false;
            this.$emit('on-ok');
            this.focus();
            this.reset();
        },
        focus() {
            this.$refs.input && this.$refs.input.focus();
        }
    },
    watch: {
        visible(state) {
            if (state === false) {
                this.$refs.drop.destroy();
            }
            this.$refs.drop.update();
            this.$emit('on-open-change', state);
        },
        value(val) {
            this.internalValue = this.parseDate(val);
        },
        open(val) {
            this.visible = val === true;
        },
        type(type) {
            this.onSelectionModeChange(type);
        },
        publicVModelValue(now, before) {
            const newValue = JSON.stringify(now);
            const oldValue = JSON.stringify(before);
            const shouldEmitInput = newValue !== oldValue || typeof now !== typeof before;
            if (shouldEmitInput) this.$emit('input', now); // to update v-model
        },
    },
    mounted() {
        const initialValue = this.value;
        const parsedValue = this.publicVModelValue;
        if (typeof initialValue !== typeof parsedValue || JSON.stringify(initialValue) !== JSON.stringify(parsedValue)) {
            this.$emit('input', this.publicVModelValue); // to update v-model
        }
        if (this.open !== null) this.visible = this.open;

        // to handle focus from confirm buttons
        this.$on('focus-input', () => this.focus());
    }
};
</script>
